// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ModelAgentBase.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Visualization
{
    using System.Runtime.Serialization;
    using Microsoft.Robotics.Runtime;

    /// <summary>
    /// Base class for Model Agents.
    /// A Model Agent is an agent that prepares data for display (but doesn't display it). 
    /// The data is stored on a data-bindable model that views can bind to. 
    /// Unlike a published state, the model instance remains the same for the lifetime of the model agent.
    /// It is up to derived classes to manage subscriptions to producer agents.
    /// </summary>
    /// <typeparam name="TMessage">The state to visualize.</typeparam>
    /// <typeparam name="TModel">The model maintained by this class.</typeparam>
    [DataContract]
    public abstract class ModelAgentBase<TMessage, TModel> : Agent,
                                                       IControlReceiver<ViewInitializationMessage>
        where TMessage : AgentMessage
        where TModel : Model, new()
    {
        /// <summary>
        /// The default update interval, if one is not specified.
        /// </summary>
        private const int DefaultUpdateInterval = 250;

        /// <summary>
        /// The interval on which to issue data binding updates to the UI
        /// </summary>
        [DataMember(Name = "UpdateIntervalMilliseconds")]
        private int maxUpdateIntervalMilliseconds;

        /// <summary>
        /// The timestamp of the last update we kept 
        /// </summary>
        private long lastTimestampInMilliseconds;

        /// <summary>
        /// The sequenceId of the last update we kept.
        /// </summary>
        private long lastSequenceId;

        /// <summary>
        /// The timestamp of the message we used to update the sample rate
        /// </summary>
        private long lastSampleRateTimestampInMilliseconds;

        /// <summary>
        /// The sequenceId of the message we used to update the sample rate
        /// </summary>
        private long lastSampleRateSequenceId;

        /// <summary>
        /// The model maintained by this agent.
        /// </summary>
        private TModel model;

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelAgentBase{TMessage,TModel}"/> class. 
        /// Initializes a new instance of the ModelAgent class
        /// </summary>
        /// <param name="name">
        /// Agent name
        /// </param>
        /// <param name="maxUpdateIntervalMilliseconds">
        /// The interval on which to issue data binding updates to the UI
        /// </param>
        public ModelAgentBase(string name, int maxUpdateIntervalMilliseconds)
            : base(name)
        {
            this.maxUpdateIntervalMilliseconds = maxUpdateIntervalMilliseconds;
        }

        /// <summary>
        /// Gets the timestamp of the last update we kept 
        /// </summary>
        protected long LastTimestampInMilliseconds
        {
            get
            {
                return this.lastTimestampInMilliseconds;
            }
        }

        /// <summary>
        /// Gets the sequenceId of the last update we kept.
        /// </summary>
        protected long LastSequenceId
        {
            get
            {
                return this.lastSequenceId;
            }
        }

        /// <summary>
        /// Gets the model maintained by this agent.
        /// </summary>
        protected TModel Model
        {
            get
            {
                return this.model;
            }
        }

        /// <summary>
        /// Called to finish initialization after the constructor has finished.
        /// </summary>
        /// <param name="locator">Locator to use.</param>
        public override void Initialize(AgentLocator locator)
        {
            base.Initialize(locator);

            this.model = new TModel();
            if (this.maxUpdateIntervalMilliseconds == 0)
            {
                this.maxUpdateIntervalMilliseconds = DefaultUpdateInterval;
            }
        }

        /// <summary>
        /// Handles updates to the messages to visualize.
        /// </summary>
        /// <param name="message">The new data to visualize.</param>
        public void Receive(TMessage message)
        {
            // throttle UI updates to the desired rate
            if ((message.Timestamp - this.LastTimestampInMilliseconds) < this.maxUpdateIntervalMilliseconds)
            {
                return;
            }

            if (message.Timestamp - this.lastSampleRateTimestampInMilliseconds > DefaultUpdateInterval)
            {
                // compute the sample rate
                double sampleRate = 1000 * (message.SequenceId - this.lastSampleRateSequenceId) / (message.Timestamp - this.lastSampleRateTimestampInMilliseconds);
                this.Model.SamplesPerSecond = sampleRate;
                this.lastSampleRateSequenceId = message.SequenceId;
                this.lastSampleRateTimestampInMilliseconds = message.Timestamp;
            }

            this.Model.MessageCount = message.SequenceId - this.Locator.CreationTime;
            this.Model.LastMessage = message;

            // let derived classes update the model data
            bool modelUpdated = this.UpdateModel(message);
            this.lastTimestampInMilliseconds = message.Timestamp;
            this.lastSequenceId = message.SequenceId;

            if (modelUpdated)
            {
                // send a notification to any the data bound controls about the model changes
                this.model.NotifyPropertyChanged();
            }
        }

        /// <summary>
        /// Called by the UI to establish a link between the model agent and the view.
        /// The model agent calls the view, providing the model instance to which the view should data-bind.
        /// </summary>
        /// <param name="message">Control message containing a reference to the view.</param>
        public void ReceiveControl(ViewInitializationMessage message)
        {
            message.View.Dispatcher.Invoke(() => message.MainWindow.BindViewAndTimerDataContextToModel(message.View, this.model));
        }

        /// <summary>
        /// Derived classes must implement this method to update their model based on the state update provided by the agent.
        /// </summary>
        /// <param name="state">The new state generated by the agent</param>
        /// <returns>true if the model was updated, false otherwise</returns>
        protected abstract bool UpdateModel(TMessage state);
    }
}
